﻿'   VBA Extractor - exports VB source code from macro enabled Word and Excel documents
'   Copyright (c) 2013 Tony Jollans & Neil Dodson

'   This program is free software; you can redistribute it and/or
'   modify it under the terms of the GNU General Public License
'   as published by the Free Software Foundation; either version 2
'   of the License, or (at your option) any later version.

'   This program is distributed in the hope that it will be useful,
'   but WITHOUT ANY WARRANTY; without even the implied warranty of
'   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
'   GNU General Public License for more details.

'   You should have received a copy of the GNU General Public License
'   along with this program; if not, write to the Free Software
'   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

Imports System.IO
Imports ICSharpCode.SharpZipLib
Imports ICSharpCode.SharpZipLib.Zip

Public Class VbaComponentFinder
    Implements IDisposable

    Private disposables As List(Of IDisposable)
    Private stream As Stream
    Private sourceFile As String
    Private isDisposed As Boolean

    Public Sub New(ByVal SourceFile As String)
        Me.sourceFile = SourceFile
        Me.disposables = New List(Of IDisposable)
    End Sub

    Protected Overridable Sub Dispose(disposing As Boolean)
        If Not Me.isDisposed Then
            If disposing Then
                If Not stream Is Nothing Then
                    For Each disposable In disposables
                        disposable.Dispose()
                    Next disposable

                    disposables.Clear()
                End If
            End If

        End If
        Me.isDisposed = True
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

    Public ReadOnly Property VbaFileStream() As Stream
        Get
            If Me.isDisposed Then
                Throw New ObjectDisposedException("stream")
            End If

            If Me.stream Is Nothing Then
                Me.stream = GetVbaFileStream(sourceFile)
            End If

            Return Me.stream
        End Get
    End Property

    Private Function GetVbaFileStream(ByVal SourceFile As String) As Stream
        Dim fileInfo As New FileInfo(SourceFile)

        If Not fileInfo.Exists Then
            Throw New FileNotFoundException("No such source file", SourceFile)
        End If

        Dim fileStream As Stream = File.Open(SourceFile, FileMode.Open, FileAccess.Read, FileShare.Read)
        disposables.Add(fileStream)

        Select Case (fileInfo.Extension.ToLower())
            Case ".bin", ".xls", ".doc"
                Return fileStream

            Case ".xlsm", ".docm", ".xlsx", ".xlsb"
                Return FindComponentInZipFile(fileStream, "vbaProject.bin")

            Case Else
                Throw New NotSupportedException(String.Format("File type '{0}' is not supported by the extractor process", fileInfo.Extension))
        End Select

    End Function

    Private Function FindComponentInZipFile(ByVal FileStream As Stream, ByVal ComponentFileName As String) As Stream

        Dim zipStream As New ZipInputStream(FileStream)
        Dim memStream As New MemoryStream
        disposables.Add(memStream)
        disposables.Add(zipStream)

        Dim anEntry As ZipEntry
        Try
            anEntry = zipStream.GetNextEntry()
        Catch
            Throw New ApplicationException("Cannot unzip Workbook  - this occurs if the workbook is protected on save")
        End Try

        Do While Not (anEntry Is Nothing)
            Debug.WriteLine("Scanning " + anEntry.Name)
            Dim pathFragments As String() = anEntry.Name.Split("/")
            If pathFragments(UBound(pathFragments)).ToLower() = ComponentFileName.ToLower() Then

                Dim buffer(4096) As Byte
                Dim readCount As Long

                Do
                    readCount = zipStream.Read(buffer, 0, buffer.Length)
                    memStream.Write(buffer, 0, readCount)
                Loop While readCount > 0

                memStream.Seek(0, SeekOrigin.Begin)
                Exit Do
            End If
            zipStream.CloseEntry()
            anEntry = zipStream.GetNextEntry()
        Loop

        If memStream.Length = 0 Then
            Throw New ApplicationException(String.Format("Could not find component '{0}' ", ComponentFileName))
        Else
            Return memStream
        End If
    End Function

End Class
